<?php

Raise::load('core.helper.RaiseHelper');
Raise::load('core.datetime.RaiseTimeSpan');
Raise::load('core.session.RaiseSession');
Raise::load('core.security.RaiseHashDigest');
Raise::load('core.collection.RaiseCollection');
Raise::load('core.datetime.RaiseDateTime');

/**
 * RaiseLimiter class
 * to prevent hacking, brute force and other request spams, it is wise to limit requests
 *
 * @author Sam-Mauris Yong / hellclanner at live dot com
 * @license http://www.opensource.org/licenses/bsd-license New BSD License
 * @package phpraise.core.security
 * @since 1.2
 */
class RaiseLimiter extends RaiseHelper {
    
    const SESSION_KEY = '_?:RaiseLimiter';

    /**
     * The identifier to use for this limiting feature
     * @var string
     */
    private $key;

    /**
     * The number of requests since last clear
     * @var integer
     */
    private $count;

    /**
     * Maximum number of requests per interval
     * @var integer
     */
    private $max;

    /**
     * The interval between each reset
     * @var RaiseTimeSpan
     */
    private $interval;

    /**
     * 
     * @var RaiseDateTime
     */
    private $lastReset;

    /**
     * The session that the information of this limiter is saved to
     * @var RaiseSession
     */
    private $session;

    /**
     * Flags whether to clear this limiting feature or not
     * @var boolean
     */
    private $clear = false;

    /**
     * Create a new RaiseLimiter
     * @param RaiseSession $session The session that the limiter will save to
     * @param string $key A key that uniquely identifies the feature that you are
     * @param integer $max The maximum number of requests that can be made within the time interval
     * @param RaiseTimeSpan $interval The interval time period between each reset
     */
    public function __construct($session, $key, $max, $interval = 60) {
        if(!($interval instanceof RaiseTimeSpan)){
            $interval = new RaiseTimeSpan($interval);
        }
        $this->key = RaiseHashDigest::sha256($key);
        $this->session = $session;
        $sessCollection = $this->session->get(self::SESSION_KEY);
        if ($sessCollection instanceof RaiseCollection && $sessCollection->get($this->key)) {
            $this->interval = new RaiseTimeSpan($sessCollection->get('interval'));
            $this->max = $sessCollection->get('max');
            $this->count = $sessCollection->get('count');
            $this->lastReset = RaiseDateTime::fromString($sessCollection->get('last'));
        }else{
            $this->interval = $interval;
            $this->max = $max;
            $this->reset();
        }
    }

    /**
     * Add request count to the limiter
     */
    public function addCount() {
        self::checkReset();
        $this->count++;
    }

    /**
     * Get the current request count
     * @return integer
     */
    public function count() {
        self::checkReset();
        return $this->count;
    }

    /**
     * Get the maximum request count
     * @return integer
     */
    public function max() {
        return $this->max;
    }

    /**
     * Get the interval time period between each reset
     * @return RaiseTimeSpan
     */
    public function interval(){
        static $c = false;
        if(!$c){
            $c = $this->interval->raiseClone();
        }
        return $c;
    }

    /**
     * Get the RaiseDateTime of the last reset
     * @return RaiseDateTime
     */
    public function lastReset(){
        static $c = false;
        if(!$c){
            $c = $this->lastReset->raiseClone();
        }
        return $c;
    }

    /**
     * Check if a reset should be done and reset if needed
     */
    private function checkReset() {
        if ($this->interval->totalSeconds() > 0 && ($this->lastReset->add($this->interval)->compareTo(RaiseDateTime::now()) == 1)) {
            $this->reset();
        }
    }

    /**
     * Force a reset
     */
    public function reset() {
        $this->count = 0;
        $this->lastReset = RaiseDateTime::now();
    }

    /**
     * Check whether is it ok to load the request
     * @return boolean FALSE if request is over limit, TRUE if it's fine.
     */
    public function check() {
        self::checkReset();
        return $this->count <= $this->max;
    }

    public function clear(){
        $this->reset();
        $this->clear = true;
    }

    /**
     * Since the object is being destructed, we'll save the information into Session
     */
    public function __destruct(){
        $sessCollection = $this->session->get(self::SESSION_KEY);
        if (!$sessCollection instanceof RaiseCollection) {
            $sessCollection = new RaiseCollection();
        }
        if($this->clear){
            $sessCollection->remove($this->key);
        }else{
            $sessCollection->add($this->key, array('max' => $this->max, 'count' => $this->count, 'last' => $this->lastReset->toISO8601(), 'interval' => $this->interval->totalSeconds()));
        }
        $this->session->set(self::SESSION_KEY, $sessCollection);
    }

}
